package com.bizmda.bizsip.app.controlrule;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.bizmda.bizsip.common.BizException;
import com.bizmda.bizsip.common.BizResultEnum;
import com.open.capacity.redis.util.RedisUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Data
@Slf4j
public class ControlRuleMetricConfig {
    private String name;
    private String desc;
    private long flidingWindowTime;
    private char fixedWindowTime;
    public static final char FIXED_WINDOW_TIME_YEAR = 'y';
    public static final char FIXED_WINDOW_TIME_MONTH = 'M';
    public static final char FIXED_WINDOW_TIME_DAY = 'd';
    public static final char FIXED_WINDOW_TIME_HOUR = 'H';
//    public static final char FIXED_WINDOW_TIME_MINUTE = 'm';
//    public static final char FIXED_WINDOW_TIME_SECOND = 's';
    private static RedisUtil redisUtil = null;
    private static RedissonClient redissonClient = null;
    private static final String PRE_REDIS_KEY = "bizsip:ctl:metric:";

    public ControlRuleMetricConfig(Map<String,Object> map) {
        this.name = (String)map.get("name");
        this.desc = (String) map.get("desc");
        this.flidingWindowTime = Convert.toLong(map.get("fliding-window-time"),0L);
        this.fixedWindowTime = Convert.toChar(map.get("fixed-window-time"),' ');
    }

    public ControlRuleMetric getMetric(String key) throws BizException {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        if (redissonClient == null) {
            redissonClient = SpringUtil.getBean(RedissonClient.class);
        }
        String mKey = PRE_REDIS_KEY+this.name+":metric:"+key;
        ControlRuleMetric controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
        if (controlRuleMetric == null) {
            return this.initMetric(key);
        }

        String lKey = PRE_REDIS_KEY+this.name+":list:"+key;
        String lockKey = PRE_REDIS_KEY+this.name+":lock:"+key;
        RLock rLock = redissonClient.getLock(lockKey);
        long now = System.currentTimeMillis();
        long count = 0;
        long amount = 0;
        for(;;) {
            long size = redisUtil.lGetListSize(lKey);
            if (size == 0) {
                break;
            }
            ControlRuleRecord controlRuleRecord1 = (ControlRuleRecord)
                    redisUtil.lGetIndex(lKey, 0L);
            if (!this.inWindowTime(now,controlRuleRecord1.getTime())) {
                if (redisUtil.lLeftPop(lKey) == null) {
                    break;
                }
                count ++;
                amount = amount + controlRuleRecord1.getAmount();
            }
            else {
                break;
            }
        }
        if (count > 0) {
            controlRuleMetric.setCount(controlRuleMetric.getCount() - count);
            controlRuleMetric.setAmount(controlRuleMetric.getAmount() - amount);
            this.lock(rLock);
            redisUtil.set(mKey, controlRuleMetric);
            this.unlock(rLock);
        }
        log.trace("getMetric({}):{}",key,controlRuleMetric);
        return controlRuleMetric;
    }

    public void addRecord(String key,long amount) throws BizException {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        if (redissonClient == null) {
            redissonClient = SpringUtil.getBean(RedissonClient.class);
        }
        String mKey = PRE_REDIS_KEY+this.name+":metric:"+key;
        ControlRuleMetric controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
        if (controlRuleMetric == null) {
            controlRuleMetric = this.initMetric(key);
        }
        ControlRuleRecord controlRuleRecord = new ControlRuleRecord();
        controlRuleRecord.setTime(System.currentTimeMillis());
        controlRuleRecord.setAmount(amount);
        String lKey = PRE_REDIS_KEY+this.name+":list:"+key;
        String lockKey = PRE_REDIS_KEY+this.name+":lock:"+key;
        RLock rLock = redissonClient.getLock(lockKey);

        this.lock(rLock);
        for(;;) {
            long size = redisUtil.lGetListSize(lKey);
            if (size == 0) {
                break;
            }
            ControlRuleRecord controlRuleRecord1 = (ControlRuleRecord)
                    redisUtil.lGetIndex(lKey, 0L);
            if (!this.inWindowTime(controlRuleRecord.getTime(), controlRuleRecord1.getTime())) {
                if (redisUtil.lLeftPop(lKey) == null) {
                    break;
                }
                controlRuleMetric.setCount(controlRuleMetric.getCount() - 1);
                controlRuleMetric.setAmount(controlRuleMetric.getAmount() - controlRuleRecord1.getAmount());
            }
            else {
                break;
            }
        }
        redisUtil.lSet(lKey,controlRuleRecord);
        controlRuleMetric.setCount(controlRuleMetric.getCount() + 1);
        controlRuleMetric.setAmount(controlRuleMetric.getAmount() + controlRuleRecord.getAmount());
        redisUtil.set(mKey,controlRuleMetric);
        this.unlock(rLock);
        log.trace("addRecord({},{}):{}",key,amount,controlRuleMetric);
        this.debugRedis(key);
    }

    public void deleteRecord(String key,long amount) throws BizException {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        if (redissonClient == null) {
            redissonClient = SpringUtil.getBean(RedissonClient.class);
        }
        String mKey = PRE_REDIS_KEY+this.name+":metric:"+key;
        ControlRuleMetric controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
        if (controlRuleMetric == null) {
            controlRuleMetric = this.initMetric(key);
        }
        String lKey = PRE_REDIS_KEY+this.name+":list:"+key;
        String lockKey = PRE_REDIS_KEY+this.name+":lock:"+key;
        RLock rLock = redissonClient.getLock(lockKey);

        long size = redisUtil.lGetListSize(lKey);
        for(long i = size-1;i>=0;i--) {
            ControlRuleRecord controlRuleRecord1 = (ControlRuleRecord)
                    redisUtil.lGetIndex(lKey, i);
            if (controlRuleRecord1.getAmount() == amount) {
                this.lock(rLock);
                long num = redisUtil.lRemove(lKey,-1,controlRuleRecord1);
                controlRuleMetric.setCount(controlRuleMetric.getCount() - num);
                controlRuleMetric.setAmount(controlRuleMetric.getAmount() - num* controlRuleRecord1.getAmount());
                redisUtil.set(mKey,controlRuleMetric);
                this.unlock(rLock);
                break;
            }
        }
        log.trace("deleteRecord({},{}):{}",key,amount,controlRuleMetric);
        this.debugRedis(key);
    }

    private ControlRuleMetric initMetric(String key) {
        return new ControlRuleMetric();
    }

    private boolean inWindowTime(long currentTime,long recordTime) {
        if (this.flidingWindowTime > 0) {
            if ((currentTime - recordTime) >= this.flidingWindowTime) {
                return false;
            }
            return true;
        }
        switch (this.fixedWindowTime) {

            case FIXED_WINDOW_TIME_YEAR:
                return DateUtil.format(DateUtil.date(currentTime),"yyyy")
                        .equals(DateUtil.format(DateUtil.date(recordTime),"yyyy"));
            case FIXED_WINDOW_TIME_MONTH:
                return DateUtil.format(DateUtil.date(currentTime),"yyyyMM")
                        .equals(DateUtil.format(DateUtil.date(recordTime),"yyyyMM"));
            case FIXED_WINDOW_TIME_DAY:
                return DateUtil.format(DateUtil.date(currentTime),"yyyyMMdd")
                        .equals(DateUtil.format(DateUtil.date(recordTime),"yyyyMMdd"));
            case FIXED_WINDOW_TIME_HOUR:
                return DateUtil.format(DateUtil.date(currentTime),"yyyyMMddHH")
                        .equals(DateUtil.format(DateUtil.date(recordTime),"yyyyMMddHH"));
        }
        return false;
    }

    private void debugRedis(String key) {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        String mKey = PRE_REDIS_KEY+this.name+":metric:"+key;
        ControlRuleMetric controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
        log.trace("{}: {}",mKey,controlRuleMetric);

        String lKey = PRE_REDIS_KEY+this.name+":list:"+key;
        long size = redisUtil.lGetListSize(lKey);
        log.trace("{}:",lKey);
        for(long i = 0;i<size;i++) {
            ControlRuleRecord controlRuleRecord1 = (ControlRuleRecord)
                    redisUtil.lGetIndex(lKey, i);
            log.trace("{}:{}",i,controlRuleRecord1);
        }
    }

    private void lock(RLock rLock) throws BizException {
        boolean res = false;
        try {
            res = rLock.tryLock(5,3,TimeUnit.SECONDS);
            if (!res) {
                throw new BizException(BizResultEnum.OTHER_LOCK_ERROR);
            }
        } catch (InterruptedException e) {
            throw new BizException(BizResultEnum.OTHER_LOCK_ERROR,e);
        }
    }

    private void unlock(RLock rLock) {
        rLock.unlock();
    }
}
